Skip to content

Added gift links admin UI for analytics, posts list, and settings#28897

Open
jonatansberg wants to merge 3 commits into
mainfrom
jonatan-ber-3729-gift-links-admin-ui-editor-settings
Open

Added gift links admin UI for analytics, posts list, and settings#28897
jonatansberg wants to merge 3 commits into
mainfrom
jonatan-ber-3729-gift-links-admin-ui-editor-settings

Conversation

@jonatansberg

Copy link
Copy Markdown
Member

ref https://linear.app/ghost/issue/BER-3729

Summary

Adds the publisher-facing gift-links admin UI on top of the service + admin API already on main. A single React gift-link modal is reused across surfaces instead of maintaining a separate Ember modal:

  • Post analytics screen — a "Share as a gift" entry in the post share modal opens the modal.
  • Ember posts/pages list — the right-click context menu fires an openGiftLinkModal event over the state bridge; a host mounted alongside the Ember fallback (at the /posts and /pages routes) opens the React modal in place.
  • Settings → Advanced → Danger zone — a "Reset all gift links" action.

All gated behind the existing private giftLinks flag.

Notes

  • The modal makes two separate reads: link details (admin API gift_links) and usage (visits/views via the same Tinybird analytics path as every other analytics surface). Usage degrades gracefully — when analytics is off, or the per-link usage pipe (BER-3746/BER-3728) isn't deployed yet, the visitor count is simply hidden and everything else still works.
  • Share URL is the canonical post URL + ?gift=<token> (no utm).
  • Posts reuse the post-analytics screen's cached query (shared POST_ANALYTICS_INCLUDE); pages fetch on their own route. API wrapper names follow the backend controllers (ensure / create / removeAll).

Testing

  • apps/posts unit suite (482) green; new URL-builder unit test; Ember eligibility-util unit test.
  • admin-x-settings danger-zone acceptance test for reset-all (passing locally).
  • Typecheck + lint clean across posts, admin, shade, admin-x-framework, admin-x-settings, and ghost/admin.

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 43d46691-4771-4191-8bca-1eb6b3b00b91

📥 Commits

Reviewing files that changed from the base of the PR and between cb0bb20 and 143b521.

📒 Files selected for processing (1)
  • apps/shade/src/components/posts-stats/post-share-modal.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/shade/src/components/posts-stats/post-share-modal.tsx

Walkthrough

This PR adds gift-link management across admin and posts. It introduces gift-link API client mutations, a settings action to reset all gift links, an Ember-to-React bridge for opening a React gift-link modal from posts and pages lists, and new posts-app hooks, utilities, modal UI, and sharing entry points. It also adds related tests, package exports, and route updates.

Possibly related PRs

  • TryGhost/Ghost#28784 — Adds the admin API client surface for gift-link operations, including resource-scoped paths and remove-all handling.
  • TryGhost/Ghost#28693 — Adds backend gift-link routes and response shapes that match the client mutations used here.
  • TryGhost/Ghost#28837 — Covers the remove-all gift-link endpoint and naming changes used by the admin reset flow.

Suggested reviewers

  • kevinansfield
  • EvanHahn
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding gift links admin UI across analytics, posts list, and settings.
Description check ✅ Passed The description matches the implemented gift-links UI work and accurately summarizes the affected surfaces.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jonatan-ber-3729-gift-links-admin-ui-editor-settings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@nx-cloud

nx-cloud Bot commented Jun 25, 2026

Copy link
Copy Markdown

🤖 Nx Cloud AI Fix

Ensure the fix-ci command is configured to always run in your CI pipeline to get automatic fixes in future runs. For more information, please see https://nx.dev/ci/features/self-healing-ci


View your CI Pipeline Execution ↗ for commit 143b521

Command Status Duration Result
nx run @tryghost/admin-x-settings:test:acceptance ✅ Succeeded 10m View ↗
nx run-many --target=build --projects=tag:publi... ✅ Succeeded 2s View ↗
nx run-many -t test:unit -p @tryghost/admin-x-f... ✅ Succeeded 7m 19s View ↗
nx run @tryghost/admin:build ✅ Succeeded 4m 26s View ↗
nx run ghost-admin:test ✅ Succeeded 3m 2s View ↗
nx run-many -t lint -p @tryghost/admin-x-framew... ✅ Succeeded 1m 10s View ↗
nx run @tryghost/activitypub:test:acceptance ✅ Succeeded 53s View ↗
nx run ghost:build:assets ✅ Succeeded 2s View ↗
nx run ghost:build:tsc ✅ Succeeded 6s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-06-29 09:10:21 UTC

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/posts/src/utils/gift-link.ts (1)

7-12: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Edge case: hard-coded ? breaks URLs that already carry a query string.

buildGiftLinkUrl always prefixes with ?, so a postUrl already containing a query string would yield a malformed URL with two ?. Canonical post URLs are normally clean, so this is just defensive hardening.

♻️ Use the correct separator
-    return `${postUrl}?gift=${encodeURIComponent(token)}`;
+    const separator = postUrl.includes('?') ? '&' : '?';
+    return `${postUrl}${separator}gift=${encodeURIComponent(token)}`;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/posts/src/utils/gift-link.ts` around lines 7 - 12, The buildGiftLinkUrl
helper always appends the gift token with a hard-coded query separator, which
breaks when postUrl already contains existing query parameters. Update
buildGiftLinkUrl to choose the correct separator based on whether postUrl
already includes a query string, and keep the token encoding behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/posts/src/views/PostAnalytics/modals/gift-link-modal.tsx`:
- Around line 135-143: Disable the ShareModal.CopyButton in gift-link-modal
while the link is being generated so it cannot copy an empty string. Update the
copy button in the gift-link CopyURLBox to use the existing ensuring state from
the modal logic, and keep the disabled state aligned with the same
giftLinkUrl/ensuring flow used in this component.

---

Nitpick comments:
In `@apps/posts/src/utils/gift-link.ts`:
- Around line 7-12: The buildGiftLinkUrl helper always appends the gift token
with a hard-coded query separator, which breaks when postUrl already contains
existing query parameters. Update buildGiftLinkUrl to choose the correct
separator based on whether postUrl already includes a query string, and keep the
token encoding behavior unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9397fe1c-217a-4d38-88dc-8ce6dd3e7870

📥 Commits

Reviewing files that changed from the base of the PR and between 515c39b and e4e3aba.

📒 Files selected for processing (24)
  • apps/admin-x-framework/src/api/gift-links.ts
  • apps/admin-x-settings/src/components/settings/advanced/danger-zone.tsx
  • apps/admin-x-settings/test/acceptance/advanced/dangerzone.test.ts
  • apps/admin/src/ember-bridge/ember-bridge.tsx
  • apps/admin/src/ember-bridge/index.ts
  • apps/admin/src/gift-link-modal-host.tsx
  • apps/admin/src/routes.tsx
  • apps/posts/package.json
  • apps/posts/src/hooks/use-can-manage-gift-link.ts
  • apps/posts/src/hooks/use-gift-link-usage.ts
  • apps/posts/src/hooks/use-post-details.ts
  • apps/posts/src/providers/post-analytics-context.tsx
  • apps/posts/src/utils/constants.ts
  • apps/posts/src/utils/gift-link.ts
  • apps/posts/src/views/PostAnalytics/components/post-analytics-header.tsx
  • apps/posts/src/views/PostAnalytics/modals/gift-link-modal.tsx
  • apps/posts/test/unit/utils/gift-link.test.ts
  • apps/shade/src/components/posts-stats/post-share-modal.tsx
  • ghost/admin/app/components/posts-list/context-menu.hbs
  • ghost/admin/app/components/posts-list/context-menu.js
  • ghost/admin/app/services/feature.js
  • ghost/admin/app/services/state-bridge.js
  • ghost/admin/app/utils/gift-link.js
  • ghost/admin/tests/unit/utils/gift-link-test.js

Comment thread apps/posts/src/views/PostAnalytics/modals/gift-link-modal.tsx

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e4e3aba258

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/posts/src/views/PostAnalytics/modals/gift-link-modal.tsx
@jonatansberg jonatansberg force-pushed the jonatan-ber-3729-gift-links-admin-ui-editor-settings branch 2 times, most recently from 88b5608 to cd8049c Compare June 25, 2026 13:01
ref https://linear.app/ghost/issue/BER-3729

- one shared React gift-link modal is reused across the post-analytics
  screen and the Ember posts/pages list, rather than maintaining a
  separate Ember modal: the list's right-click menu fires an
  `openGiftLinkModal` event over the state bridge and a host mounted
  alongside the Ember fallback opens the React modal in place
- the modal fetches link details (admin API) and usage (the same
  Tinybird analytics path as everything else) separately, degrading to
  no visitor count when analytics is off or the usage pipe isn't
  deployed yet; the share URL is the canonical post URL + `?gift=<token>`
- adds the danger-zone "reset all gift links" action and a "share as a
  gift" entry in the post share modal
- all gated behind the existing private `giftLinks` flag
@jonatansberg jonatansberg force-pushed the jonatan-ber-3729-gift-links-admin-ui-editor-settings branch from cd8049c to 0f9b693 Compare June 25, 2026 14:30

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0f9b6932f5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}), [statsConfig?.id, postUuid]);

const {data, loading, error} = useTinybirdQuery({
endpoint: 'api_gift_link_visits',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add the Tinybird pipe before querying it

With analytics enabled, this hook asks Tinybird for api_gift_link_visits, but repo-wide rg "api_gift_link_visits" ghost/core/core/server/data/tinybird finds no endpoint datafile, while getStatEndpointUrl resolves endpoint names to /v0/pipes/<name>.json. As a result every gift-link card/modal calls a non-existent pipe and the visitor count stays hidden/ even when visits exist; add the Tinybird endpoint (and any versioned variants required by statsConfig.version) or gate this query until it exists.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/posts/src/views/PostAnalytics/Overview/overview.tsx`:
- Around line 218-225: The “Share” button in PostAnalytics/Overview is only
revealed on hover, so it stays hidden for keyboard and touch users. Update the
Button in the relevant render path to also reveal on focus-visible (mirroring
the existing Growth “View more” behavior if applicable), while keeping the
current hover animation intact. Use the Button element and its existing
className/variant setup as the reference point, and apply the same accessibility
fix wherever the same hover-only pattern appears.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 718f5ac2-0d5e-458f-b497-8aa983b49480

📥 Commits

Reviewing files that changed from the base of the PR and between cd8049c and 0f9b693.

📒 Files selected for processing (25)
  • apps/admin-x-framework/src/api/gift-links.ts
  • apps/admin-x-settings/src/components/settings/advanced/danger-zone.tsx
  • apps/admin-x-settings/test/acceptance/advanced/dangerzone.test.ts
  • apps/admin/src/ember-bridge/ember-bridge.tsx
  • apps/admin/src/ember-bridge/index.ts
  • apps/admin/src/gift-link-modal-host.tsx
  • apps/admin/src/routes.tsx
  • apps/posts/package.json
  • apps/posts/src/hooks/use-can-manage-gift-link.ts
  • apps/posts/src/hooks/use-gift-link-usage.ts
  • apps/posts/src/hooks/use-post-details.ts
  • apps/posts/src/providers/post-analytics-context.tsx
  • apps/posts/src/utils/constants.ts
  • apps/posts/src/utils/gift-link.ts
  • apps/posts/src/views/PostAnalytics/Overview/overview.tsx
  • apps/posts/src/views/PostAnalytics/components/post-analytics-header.tsx
  • apps/posts/src/views/PostAnalytics/modals/gift-link-modal.tsx
  • apps/posts/test/unit/utils/gift-link.test.ts
  • apps/shade/src/components/posts-stats/post-share-modal.tsx
  • ghost/admin/app/components/posts-list/context-menu.hbs
  • ghost/admin/app/components/posts-list/context-menu.js
  • ghost/admin/app/services/feature.js
  • ghost/admin/app/services/state-bridge.js
  • ghost/admin/app/utils/gift-link.js
  • ghost/admin/tests/unit/utils/gift-link-test.js
✅ Files skipped from review due to trivial changes (3)
  • apps/admin/src/ember-bridge/index.ts
  • apps/posts/src/providers/post-analytics-context.tsx
  • apps/posts/test/unit/utils/gift-link.test.ts
🚧 Files skipped from review as they are similar to previous changes (21)
  • apps/admin-x-settings/test/acceptance/advanced/dangerzone.test.ts
  • apps/posts/src/utils/gift-link.ts
  • apps/posts/package.json
  • apps/posts/src/hooks/use-can-manage-gift-link.ts
  • ghost/admin/app/components/posts-list/context-menu.hbs
  • ghost/admin/app/services/feature.js
  • apps/admin/src/routes.tsx
  • apps/admin/src/ember-bridge/ember-bridge.tsx
  • apps/admin-x-framework/src/api/gift-links.ts
  • apps/posts/src/hooks/use-post-details.ts
  • apps/posts/src/views/PostAnalytics/components/post-analytics-header.tsx
  • apps/posts/src/utils/constants.ts
  • ghost/admin/app/utils/gift-link.js
  • ghost/admin/app/components/posts-list/context-menu.js
  • apps/shade/src/components/posts-stats/post-share-modal.tsx
  • ghost/admin/app/services/state-bridge.js
  • ghost/admin/tests/unit/utils/gift-link-test.js
  • apps/posts/src/hooks/use-gift-link-usage.ts
  • apps/admin-x-settings/src/components/settings/advanced/danger-zone.tsx
  • apps/admin/src/gift-link-modal-host.tsx
  • apps/posts/src/views/PostAnalytics/modals/gift-link-modal.tsx

Comment on lines +218 to +225
<Button
className='absolute right-6 translate-x-10 opacity-0 transition-all duration-300 group-hover/datalist:translate-x-0 group-hover/datalist:opacity-100'
size='sm'
variant='outline'
onClick={() => setIsGiftLinkOpen(true)}
>
Share
</Button>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

“Share” button is only visible on hover, with no keyboard/touch fallback.

opacity-0 + group-hover/datalist:opacity-100 leaves the button focusable and clickable but invisible for keyboard users (focus lands on an invisible control) and touch devices (no hover). Adding a focus-visible reveal keeps it discoverable while preserving the hover animation. This mirrors the existing Growth “View more” button, so consider it there too.

♿ Reveal on focus as well as hover
                                         <Button
-                                            className='absolute right-6 translate-x-10 opacity-0 transition-all duration-300 group-hover/datalist:translate-x-0 group-hover/datalist:opacity-100'
+                                            className='absolute right-6 translate-x-10 opacity-0 transition-all duration-300 group-hover/datalist:translate-x-0 group-hover/datalist:opacity-100 focus-visible:translate-x-0 focus-visible:opacity-100'
                                             size='sm'
                                             variant='outline'
                                             onClick={() => setIsGiftLinkOpen(true)}
                                         >
                                             Share
                                         </Button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Button
className='absolute right-6 translate-x-10 opacity-0 transition-all duration-300 group-hover/datalist:translate-x-0 group-hover/datalist:opacity-100'
size='sm'
variant='outline'
onClick={() => setIsGiftLinkOpen(true)}
>
Share
</Button>
<Button
className='absolute right-6 translate-x-10 opacity-0 transition-all duration-300 group-hover/datalist:translate-x-0 group-hover/datalist:opacity-100 focus-visible:translate-x-0 focus-visible:opacity-100'
size='sm'
variant='outline'
onClick={() => setIsGiftLinkOpen(true)}
>
Share
</Button>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/posts/src/views/PostAnalytics/Overview/overview.tsx` around lines 218 -
225, The “Share” button in PostAnalytics/Overview is only revealed on hover, so
it stays hidden for keyboard and touch users. Update the Button in the relevant
render path to also reveal on focus-visible (mirroring the existing Growth “View
more” behavior if applicable), while keeping the current hover animation intact.
Use the Button element and its existing className/variant setup as the reference
point, and apply the same accessibility fix wherever the same hover-only pattern
appears.

weylandswart and others added 2 commits June 25, 2026 17:04
ref https://linear.app/ghost/issue/BER-3729

- dropped the extra flex wrapper around ShareModal.Footer + the gift link
- ShareModal.Content (a DialogContent) already lays its children out in a
  grid with gap-6, so the wrapper only duplicated that spacing (with an
  inconsistent gap-5) and broke the pattern where each ShareModal.* section
  is a direct child of Content

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 143b521979

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Visitors
</span>
<span className='text-[2.2rem] leading-none font-semibold'>
{formatNumber(giftLinkUsage?.visits || 0)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Don't report unknown gift-link usage as zero

When analytics is disabled or the Tinybird request errors, useGiftLinkUsage returns undefined to distinguish “count unavailable” from a successful zero. This fallback coerces that state to 0, so the overview card tells users there were no gift-link visitors even when the count is unavailable; render a placeholder or hide the metric until usage is defined.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants